如何设计一个类似Dropbox的文件托管服务?
点击关注公众号,AI,编程干货及时送达
让我们设计一个类似于Dropbox或Google Drive的文件托管服务。云文件存储允许用户将他们的数据存储在远程服务器上。这些服务器通常由云存储服务提供商维护,并通过网络(通常是通过互联网)向用户提供。用户按月支付他们的云数据存储费用。
类似的服务:OneDrive,Google Drive难度
级别:中等
1. 为什么选择云存储?
近期,云文件存储服务变得非常受欢迎,因为它们简化了在多个设备之间存储和交换数字资源的过程。从使用单一的个人电脑转变为使用多种平台和操作系统的多个设备,如智能手机和平板电脑,每个设备都可以在任何时间、任何地点进行便携式访问,这被认为是云存储服务巨大受欢迎的原因。以下是这类服务的一些主要优点:
可用性:云存储服务的座右铭是“随时随地可用的数据”。用户可以在任何时候、任何地点、任何设备上访问他们的文件/照片。
可靠性和持久性:云存储的另一个优点是它能提供100%的数据可靠性和持久性。云存储确保用户通过在不同地理位置的服务器上保持数据的多个副本,永远不会丢失他们的数据。
可扩展性:用户永远不必担心存储空间不足。只要你愿意支付费用,云存储就可以提供无限的存储空间。
如果你之前没有使用过dropbox.com,我们强烈建议你在那里创建一个账户并上传/编辑文件,同时了解他们的服务提供的不同选项。这将在理解本章节时给你很大的帮助。
2. 系统的需求和目标
💡你应该在面试开始时明确需求。一定要提问以找出面试官心中系统的确切范围。我们希望从一个云存储系统中实现什么?以下是我们系统的顶级需求:
1. 用户应能够从任何设备上传和下载他们的文件/照片。
2. 用户应能够与其他用户分享文件或文件夹。
3. 我们的服务应支持设备间的自动同步,即,在一个设备上更新文件后,它应在所有设备上进行同步。
4. 系统应支持存储高达GB的大文件。
5. 需要ACID性。所有文件操作的原子性、一致性、隔离性和持久性应得到保证。
6. 我们的系统应支持离线编辑。用户应能够在离线时添加/删除/修改文件,一旦他们上线,所有的更改应该同步到远程服务器和其他在线设备。
扩展需求
• 系统应支持数据的快照,这样用户可以回退到文件的任何版本。### 3. 一些设计考虑
• 我们应该预计会有大量的读写量。
• 读写比预计将近乎相同。
• 在内部,文件可以存储在小部分或块(比如说4MB)中;这可以提供很多好处,比如,所有失败的操作只需为文件的较小部分重新尝试。如果用户未能上传文件,那么只需重新尝试失败的块。
• 我们可以通过仅传输更新的块来减少数据交换的量。
• 通过删除重复的块,我们可以节省存储空间和带宽使用。
• 在客户端保留元数据(文件名,大小等)的本地副本可以节省很多服务器的往返时间。
• 对于小的改动,客户端可以智能地上传差异部分,而不是整个块。
3. 考虑事项
• 我们应该预计到大量的读写操作。
• 读写比例预计将几乎相同。
• 在内部,文件可以分为小部分或块(例如4MB)存储;这样可以带来许多好处,例如所有失败的操作只需要对文件的较小部分重试。如果用户上传文件失败,那么只有失败的块将被重试。
• 我们可以通过仅传输更新的块来减少数据交换的量。
• 通过删除重复的块,我们可以节省存储空间和带宽使用。
• 在客户端保留元数据的本地副本(文件名、大小等)可以节省很多与服务器的往返次数。
• 对于小的更改,客户端可以智能地上传差异而不是整个块。
4. 容量估计和约束
• 让我们假设我们总共有5亿用户,每日活跃用户(DAU)有1亿。
• 假设平均每个用户从三个不同的设备上连接。
• 如果平均每个用户有200个文件/照片,我们总共将有1000亿个文件。
• 假设平均文件大小为100KB,这将给我们带来10PB的总存储。
1000亿 * 100KB => 10PB
• 我们还假设每分钟将有一百万的活跃连接。
5. 高级设计
用户将在他们的设备上指定一个文件夹作为工作区。放在这个文件夹中的任何文件/照片/文件夹都将被上传到云端,而且每当文件被修改或删除时,都会在云存储中以相同的方式反映出来。用户可以在他们所有的设备上指定类似的工作区,任何在一个设备上完成的修改都将传播到所有其他设备上,使得工作区的视图在所有地方都保持一致。
从高层次来看,我们需要存储文件及其元数据信息,如文件名、文件大小、目录等,以及与谁分享了这个文件。所以,我们需要一些服务器来帮助客户端将文件上传/下载到云存储,还需要一些服务器来帮助更新有关文件和用户的元数据。我们还需要一种机制,每当更新发生时就通知所有的客户端,这样他们就可以同步他们的文件。
如下图所示,块服务器将与客户端一起从云存储中上传/下载文件,元数据服务器将在SQL或NoSQL数据库中更新文件的元数据。同步服务器将处理通知所有客户端进行不同更改以进行同步的工作流。
6. 组件设计
让我们逐一过一下我们系统的主要组件:
a. 客户端
客户端应用程序监控用户机器上的工作区文件夹,并将其中的所有文件/文件夹与远程云存储同步。客户端应用程序将与存储服务器一起工作,上传、下载和修改后端云存储的实际文件。客户端还与远程同步服务进行交互,处理任何文件元数据的更新,例如文件名、大小、修改日期等的变化。
这里是客户端的一些关键操作:
1. 上传和下载文件。
2. 在工作区文件夹中检测文件变化。
3. 处理因离线或并发更新产生的冲突。
我们如何有效地处理文件传输?如上所述,我们可以将每个文件分解成较小的块,这样我们只需传输被修改的块,而不是整个文件。比方说我们将每个文件分割成固定大小的4MB的块。我们可以静态计算出最优的块大小,基于1)我们在云中使用的存储设备,以优化空间利用率和每秒输入/输出操作(IOPS),2)网络带宽,3)存储中的平均文件大小等。在我们的元数据中,我们也应该记录每个文件及其构成的块。
我们是否应该在客户端保存元数据的副本?保留元数据的本地副本不仅可以让我们进行离线更新,还可以节省许多更新远程元数据的往返时间。
客户端如何有效地监听其他客户端的变化?一种解决方案可能是,客户端定期检查服务器是否有任何变化。这种方法的问题在于,我们在本地反映变化时会有延迟,因为客户端将定期检查变化,而不是服务器在有变化时进行通知。如果客户端经常检查服务器的变化,不仅会浪费带宽,因为服务器大多数时间必须返回空响应,而且还会让服务器繁忙。以这种方式拉取信息是不可扩展的。
解决上述问题的一种方法可能是使用HTTP长轮询。在长轮询中,客户端从服务器请求信息,期待服务器可能不会立即响应。如果服务器在接收到轮询时没有新的数据给客户端,那么它不会发送一个空响应,而是保持请求打开,等待响应信息可用。一旦有新信息,服务器立即向客户端发送HTTP/S响应,完成开放的HTTP/S请求。客户端在收到服务器响应后,可以立即向服务器请求未来的更新。
基于上述考虑,我们可以将我们的客户端划分为以下四个部分:
I. 内部元数据数据库将跟踪所有文件、块、它们的版本和在文件系统中的位置。
II. 分块器会将文件分割成称为块的小块。它也负责从它的块中重构文件。我们的分块算法将检测用户修改的文件的部分,并只将那些部分传输到云存储;这将节省我们的带宽和同步时间。
III. 监视器将监视本地工作区文件夹,并通知索引器(如下所述)用户执行的任何操作,例如当用户创建、删除或更新文件或文件夹。监视器也监听由同步服务广播的在其他客户端发生的任何变化。
IV. 索引器将处理来自监视器的事件,并用修改文件的块的信息更新内部元数据数据库。一旦块成功提交/下载到云存储,索引器将与远程同步服务进行通信,广播到其他客户端的更改和更新远程元数据数据库。
客户端应如何处理慢服务器?如果服务器繁忙/不响应,客户端应指数级地退避。也就是说,如果服务器响应太慢,客户端应该延迟重试,这个延迟应该呈指数级增加。
移动客户端应立即同步远程变更吗?与桌面或网络客户端不同,移动客户端通常需要按需同步以节省用户的带宽和空间。
b. 元数据数据库(Metadata Database)
元数据数据库负责维护文件/块(chunk)、用户和工作区的版本和元数据信息。元数据数据库可以是像MySQL这样的关系数据库,或者是像DynamoDB这样的NoSQL数据库服务。无论数据库的类型如何,同步服务(Synchronization Service)都应该能够使用数据库提供文件的一致视图,特别是当多个用户同时处理同一个文件时。由于NoSQL数据存储不支持ACID属性,以支持可伸缩性和性能,所以如果我们选择这种类型的数据库,我们需要在同步服务的逻辑中以编程方式加入对ACID属性的支持。然而,使用关系数据库可以简化同步服务的实现,因为它们天然支持ACID属性。
元数据数据库应存储以下对象的信息:
1. 块(Chunks)
2. 文件(Files)
3. 用户(User)
4. 设备(Devices)
5. 工作空间(Workspace,同步文件夹)
c. 同步服务(Synchronization Service)
同步服务是处理由客户端做出的文件更新并将这些更改应用到其他订阅的客户端的组件。它还将客户端的本地数据库与存储在远程元数据DB中的信息同步。由于同步服务在管理元数据和同步用户文件中的关键角色,它是系统架构中最重要的部分。桌面客户端与同步服务通信,以从云存储中获取更新,或者将文件和更新发送到云存储,可能还会发送给其他用户。如果一个客户端在一段时间内离线,它会在上线后尽快向系统请求新的更新。当同步服务接收到一个更新请求时,它会检查元数据数据库的一致性,然后进行更新。随后,向所有订阅的用户或设备发送通知,报告文件更新。
同步服务的设计应该以使得在客户端和云存储之间传输更少的数据,以达到更好的响应时间。为了达到这个设计目标,同步服务可以使用差分算法来减少需要同步的数据量。我们可以只传输文件的两个版本之间的差异,而不是从客户端到服务器或反之传输整个文件。因此,只有文件改变的部分会被传输。这也会降低带宽消耗和最终用户的云数据存储。如上述描述,我们将文件分割成4MB的块,并只传输修改过的块。服务器和客户端可以计算一个哈希(例如,SHA-256)来判断是否更新本地的一个块。在服务器上,如果我们已经有一个具有类似哈希的块(即使来自另一个用户),我们不需要创建另一个副本,我们可以使用同一个块。这在后面的数据去重(Data Deduplication)部分中详细讨论。
为了能够提供一个高效和可伸缩的同步协议,我们可以考虑在客户端和同步服务之间使用一个通信中间件。消息中间件应提供可扩展的消息队列和变更通知,以支持使用拉取(pull)或推送(push)策略的大量客户端。这样,多个同步服务实例可以从全局请求队列接收请求,通信中间件将能够平衡其负载。
d. 消息队列服务(Message Queuing Service)
我们架构中的一个重要部分是能够处理大量请求的消息中间件。支持客户端和同步服务之间的异步消息通信的可伸缩消息队列服务,最适合我们应用的需求。消息队列服务支持系统分布式组件之间的异步和松耦合消息通信。消息队列服务应该能够有效地在一个高可用、可靠和可扩展的队列中存储任何数量的消息。
在我们的系统中,消息队列服务将实现两种类型的队列。请求队列(Request Queue)是一个全局队列,所有客户端都将共享它。客户端更新元数据数据库的请求首先会被发送到请求队列,然后同步服务会从那里取走它以更新元数据。对应于各个订阅客户端的响应队列(Response Queues)负责将更新消息传递给每个客户端。由于一条消息一旦被客户端接收,就会从队列中删除,所以我们需要为每个订阅的客户端创建单独的响应队列,以共享更新消息。
e. 云/块存储(Cloud/Block Storage)
云/块存储存储用户上传的文件块。客户端直接与存储交互,从中发送和接收对象。将元数据与存储分离,使我们可以在云或本地使用任何存储。
7. 文件处理工作流(File Processing Workflow)
下面的顺序显示了应用组件在客户端A更新与客户端B和C共享的文件的场景下的交互,所以他们也应该接收到更新。如果其他客户端在更新时没有在线,那么消息队列服务会将更新通知保留在他们的独立响应队列中,直到他们后来上线。
1. 客户端A上传块到云存储。
2. 客户端A更新元数据并提交更改。
3. 客户端A得到确认,并向客户端B和C发送关于更改的通知。
4. 客户端B和C接收元数据更改并下载更新的块。
8. 数据去重(Data Deduplication)
数据去重是一种用于消除数据的重复副本以提高存储利用率的技术。它也可以应用于网络数据传输,以减少必须发送的字节数。对于每一个新进来的块,我们可以计算它的哈希值,并与所有已存在的块的哈希值进行比较,看看我们的存储中是否已经有相同的块。
我们可以在系统中以两种方式实现去重:
a. 后处理去重(Post-process deduplication) 在后处理去重中,新的块首先被存储在存储设备上,然后某个进程分析数据寻找重复的部分。其优点是客户端在存储数据前不需要等待哈希计算或查找完成,因此保证了存储性能不降低。这种方法的缺点是1) 我们会不必要地存储重复数据,虽然只是短时间,2) 重复数据会消耗带宽。
b. 内联去重(In-line deduplication) 或者,我们可以在客户端在其设备上输入数据时实时进行去重哈希计算。如果我们的系统识别出一个已经存储的块,那么在元数据中只会添加一个对现有块的引用,而不是块的完整副本。这种方法将为我们提供最优的网络和存储使用。
9. 元数据分区(Metadata Partitioning)
为了扩展元数据数据库(Metadata DB),我们需要对它进行分区,以便它能存储关于数百万用户和数十亿文件/块的信息。我们需要提出一个分区方案,将我们的数据划分并存储在不同的数据库服务器中。
1. 垂直分区(Vertical Partitioning): 我们可以对数据库进行分区,以便我们将与一个特定功能相关的表存储在一个服务器上。例如,我们可以将所有与用户相关的表存储在一个数据库中,将所有与文件/块相关的表存储在另一个数据库中。虽然这种方法的实现很直接,但它存在一些问题:
1. 我们还会有规模问题吗?如果我们有万亿级的块需要存储,而我们的数据库不能支持存储如此庞大数量的记录,我们如何进一步划分这样的表?
2. 在两个独立的数据库中连接两个表可能会导致性能和一致性问题。我们需要多频繁地连接用户和文件表?
2. 基于范围的分区(Range Based Partitioning): 如果我们根据文件路径(File Path)的首字母将文件/块存储在单独的分区中会怎样?在这种情况下,我们将所有以字母‘A’开头的文件保存在一个分区中,那些以字母‘B’开头的保存在另一个分区中,依此类推。这种方法被称为基于范围的分区。我们甚至可以将某些出现频率较低的字母合并到一个数据库分区中。我们应该静态地提出这种分区方案,这样我们就可以以一种可预测的方式存储/查找文件。
这种方法的主要问题是可能导致服务器负载不平衡。例如,如果我们决定将所有以字母‘E’开头的文件放入一个数据库分区,并且后来我们发现我们有太多以字母‘E’开头的文件,到达我们无法将它们都放入一个数据库分区的程度。
1. 基于哈希的分区(Hash-Based Partitioning): 在这种方案中,我们对我们正在存储的对象取哈希,然后根据这个哈希确定该对象应去哪个数据库分区。在我们的情况下,我们可以取我们正在存储的文件对象的‘FileID’的哈希来确定存储文件的分区。我们的哈希函数将随机地将对象分布到不同的分区,例如,我们的哈希函数可以始终将任何ID映射到[1...256]之间的一个数字,这个数字就是我们将存储对象的分区。
这种方法仍然可能导致分区负载过重,这可以通过使用一致性哈希(Consistent Hashing)来解决。
10. 缓存(Caching)
我们的系统中可以有两种类型的缓存。为了处理热门文件/块,我们可以为块存储(Block storage)引入一个缓存。我们可以使用像Memcached这样的现成解决方案,它可以在访问块存储前先将整个块及其相应的ID/Hashes和块服务器存储在缓存中,以快速检查缓存是否有所需的块。根据客户端的使用模式,我们可以确定我们需要多少缓存服务器。一台高端的商用服务器可以拥有144GB的内存;这样的服务器可以缓存36K个块。
哪种缓存替换策略最适合我们的需求?当缓存满了,我们想要用一个更新/更热的块来替换一个块,我们应该如何选择?最近最少使用(LRU)可能是我们系统的一个合理策略。根据这个策略,我们首先丢弃最近最少使用的块。同样,我们也可以为元数据数据库(Metadata DB)设置一个缓存。
11. 负载均衡器(Load Balancer, LB)
我们可以在系统的两个地方添加负载均衡层:1) 客户端和块服务器(Block servers)之间,2) 客户端和元数据服务器(Metadata servers)之间。起初,可以采取简单的轮询法(Round Robin),将接入的请求平均分配到后端服务器中。这个负载均衡器易于实现且不会带来任何额外开销。这种方法的另一个好处是,如果服务器出故障,负载均衡器会将其从轮询队列中移出,停止向其发送任何流量。轮询负载均衡器的一个问题是,它不会考虑服务器的负载。如果服务器负载过重或运行缓慢,负载均衡器不会停止向该服务器发送新请求。为了解决这个问题,可以放置一个更智能的负载均衡器解决方案,它会定期查询后端服务器的负载情况,并根据这些信息调整流量。
12. 安全性、权限和文件分享
用户在云端存储文件时的主要关注点之一是他们的数据的隐私和安全性,尤其是在我们的系统中,用户可以与其他用户分享他们的文件,甚至可以将它们设为公开,与所有人分享。为了解决这个问题,我们将在我们的元数据数据库中存储每个文件的权限,以反映哪些文件对任何用户可见或可修改。
推荐阅读
你好,我是拾叁,7年开发老司机、互联网两年外企5年。怼得过阿三老美,也被PR comments搞崩溃过。这些年我打过工,创过业,接过私活,也混过upwork。赚过钱也亏过钱。一路过来,给我最深的感受就是不管学什么,一定要不断学习。只要你能坚持下来,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯和编程知识,帮你积累弯道超车的资本。